package com.conversationboard.model;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.apache.commons.collections.map.ListOrderedMap;

import com.conversationboard.database.Database;
import com.conversationboard.database.Queries;
import com.conversationboard.tree.Folders;
import com.conversationboard.tree.Node;
import com.conversationboard.tree.NodeType;

/**
 * Cache of all the available boards. Locking on returning boards doesn't really
 * do you a lot, since the board could be modified once returned and you're
 * iterating over it. Really ought to make a copy; however, it's really not
 * worth it given the proportion of reads to writes is *overwhelmingly* in the
 * favour of reads.
 * 
 * This is actually a memory leak, this class, in that the board cache can grow indefinitely.
 * It is my assumption that one instance of conversationboard will not service tens of thousands
 * of message boards!
 * 
 * @author Keith Watson
 */

public final class Boards {

	private static ListOrderedMap boards = new ListOrderedMap();
	private static ReentrantReadWriteLock lockManager = new ReentrantReadWriteLock();


	private Boards() {
	}


	public static void refreshCache() throws SQLException {

		Lock lock = lockManager.writeLock();
		lock.lock();

		try {
			boards.clear();
			getAllBoards();
		} finally {
			lock.unlock();
		}

	}


	public static Board getBoard(int boardId) throws SQLException {

		if (boards.isEmpty()) {
			getAllBoards();
		}

		Lock lock = lockManager.readLock();
		lock.lock();

		try {
			Board board = (Board) boards.get((Integer) boardId);

			return board;
		} finally {
			lock.unlock();
		}

	}


	/**
	 * Return a list of the boards you have access to, i.e. all the public boards, but also the
	 * private boards that you have access to.
	 * 
	 * @param user
	 * @return
	 */

	public static List<Board> getAuthorisedBoards(User user, boolean returnHiddenBoards) throws SQLException {

		List<Board> allBoards = Boards.getAllBoards();
		List<Board> boards = new ArrayList<Board>(allBoards.size());

		if (user == null) {
			user = User.getAnonymousUser();
		}

		for (Board board : allBoards) {

			if (board.isHidden() && (!returnHiddenBoards)) {
				continue;
			}

			if (board.isPrivateBoard() && (user.isMemberOfBoard(board.getBoardId()))) {
				boards.add(board);
			}

			if (board.isInviteOnly()) {
				boards.add(board);
			}

			if (board.isPublicBoard()) {
				boards.add(board);
			}

		}

		return boards;
	}


	/**
	 * Return boards in the following order:
	 * 
	 * (1) Popular boards
	 * (2) Private or Invite Only boards
	 * (3) Remaining boards
	 * 
	 * @param boards
	 * @return
	 */

	public static List<Board> orderBoards(List<Board> boards) {

		List<Board> orderedBoards = new LinkedList<Board>();
		Map<Integer, Integer> alreadyMoved = new HashMap<Integer, Integer>();

		for (Board board : boards) {
			if (board.isPopularBoard()) {
				if (!alreadyMoved.containsKey(board.getBoardId())) {
					orderedBoards.add(board);
					alreadyMoved.put(board.getBoardId(), board.getBoardId());
				}
			}
		}

		for (Board board : boards) {
			if (board.isPrivateBoard() || board.isInviteOnly()) {
				if (!alreadyMoved.containsKey(board.getBoardId())) {
					orderedBoards.add(board);
					alreadyMoved.put(board.getBoardId(), board.getBoardId());
				}
			}
		}

		for (Board board : boards) {
			if (!alreadyMoved.containsKey(board.getBoardId())) {
				orderedBoards.add(board);
				alreadyMoved.put(board.getBoardId(), board.getBoardId());
			}
		}

		return orderedBoards;

	}


	/**
	 * Returns all boards, including private and hidden boards.
	 * 
	 * @return
	 * @throws SQLException
	 */

	public static List<Board> getAllBoards() throws SQLException {

		Connection connection = null;

		/* First, check in the cache for the boards */

		if (!boards.isEmpty()) {

			Lock lock = lockManager.readLock();
			lock.lock();

			try {
				List<Board> list = new ArrayList<Board>(boards.valueList());
				return list;
			} finally {
				lock.unlock();
			}
		}

		/* If you don't find the boards, go to the database to get them there,
		 * and then place them in the cache */

		Lock lock = lockManager.writeLock();
		lock.lock();

		try {
			connection = Database.getConnection();

			PreparedStatement statement = connection.prepareStatement(Queries.GET_ALL_BOARDS_QUERY);

			statement.execute();
			ResultSet resultSet = statement.getResultSet();

			while (resultSet.next()) {

				Board board = new Board();
				board.setBoardId(resultSet.getInt("boardid"));
				board.setName(resultSet.getString("name"));
				board.setDescription(resultSet.getString("description"));
				board.setCreationDate(resultSet.getTimestamp("creationdate"));
				board.setPopularBoard(resultSet.getBoolean("popularboard"));
				board.setBoardAccess(BoardAccess.valueOf(resultSet.getString("access")));
				board.setRequireRegistration(resultSet.getBoolean("requireregistration"));
				board.setHidden(resultSet.getBoolean("hidden"));
				board.setAdminsAddUsers(resultSet.getBoolean("adminsaddusers"));
				board.setAllowPolls(resultSet.getBoolean("allowpolls"));
				board.setNoIndex(resultSet.getBoolean("noindex"));
				board.setLogAdminActions(resultSet.getBoolean("logadminactions"));
				board.setSandboxDays(resultSet.getInt("sandboxdays"));
				board.setNumberOfNewPosts(resultSet.getInt("numberofnewposts"));

				boards.put(board.getBoardId(), board);
			}

			statement.close();

			List<Board> list = new ArrayList<Board>(boards.valueList());

			return list;

		} finally {
			Database.close(connection);
			lock.unlock();
		}
	}


	/**
	 * Deletes a board. Mainly used for testing. ACTUALLY deletes the board, the
	 * threads contained within and all the messages forever. Use with extreme
	 * caution.
	 * 
	 * @param boardId
	 *            the ID of the board
	 */

	public static void delete(int boardId) throws SQLException {

		Lock lock = lockManager.writeLock();
		lock.lock();

		Connection connection = Database.getConnection();

		try {

			/*
			 * This could be better (i.e. one query) if I spend a bit of time
			 * messing about with outer joins and the like. There is a danger
			 * this will leave orphaned data, because of MyISAM's lack of
			 * transactionality; however, it's fairly unlikely to fail.
			 */

			PreparedStatement statement = connection.prepareStatement(Queries.PERMANENTLY_DELETE_BOARD);
			statement.setInt(1, boardId);
			statement.executeUpdate();
			statement.close();

			PreparedStatement statement2 = connection.prepareStatement(Queries.PERMANENTLY_DELETE_THREADS);
			statement2.setInt(1, boardId);
			statement2.executeUpdate();
			statement2.close();

			PreparedStatement statement3 = connection.prepareStatement(Queries.PERMANENTLY_DELETE_MESSAGES);
			statement3.setInt(1, boardId);
			statement3.executeUpdate();
			statement3.close();

			/* Remove from the board cache too (the new answers cache and
			 * message caches will catch up in due time; none of the
			 * threads/messages will be being referenced. */

			boards.remove((Integer) boardId);

		} finally {
			Database.close(connection);
			lock.unlock();
		}

	}


	/**
	 * Create a new board. Add it to the database and to the cache.
	 * 
	 * @param name
	 *            the name of the board
	 * @param description
	 *            the board's description
	 * @return the key on the database (mainly for testing purposes)
	 * @throws SQLException
	 */

	public static int create(String name, String description, BoardAccess boardAccess, boolean popular, boolean requireRegistration, boolean hidden, boolean adminsAddUsers, boolean allowPolls, boolean noIndex, boolean logAdminActions, int sandboxDays, int numberOfNewPosts) throws SQLException {

		Lock lock = lockManager.writeLock();
		lock.lock();

		Connection connection = Database.getConnection();

		try {

			Random random = new Random(System.nanoTime());
			int boardKey = random.nextInt(2000000000);

			/* Add the new board to the database */

			PreparedStatement statement = connection.prepareStatement(Queries.ADMIN_INSERT_BOARD);
			statement.setInt(1, boardKey);
			statement.setString(2, name);
			statement.setString(3, description);
			statement.setBoolean(4, popular);
			statement.setString(5, boardAccess.toString());
			statement.setBoolean(6, requireRegistration);
			statement.setBoolean(7, hidden);
			statement.setBoolean(8, adminsAddUsers);
			statement.setBoolean(9, allowPolls);
			statement.setBoolean(10, noIndex);
			statement.setBoolean(11, logAdminActions);
			statement.setInt(12, sandboxDays);
			statement.setInt(13, numberOfNewPosts);

			statement.executeUpdate();
			statement.close();

			/* Now add the board to the cache */

			Board board = new Board();
			board.setBoardId(boardKey);
			board.setName(name);
			board.setDescription(description);
			board.setCreationDate(new Date());
			board.setPopularBoard(false);
			board.setBoardAccess(boardAccess);
			board.setHidden(hidden);
			board.setAdminsAddUsers(adminsAddUsers);
			board.setAllowPolls(allowPolls);
			board.setAllowPolls(noIndex);

			boards.put(boardKey, board);
			
			/* Add this board as a child of the root */
			
			Node node = new Node(Folders.getInstance().getRoot());
			node.setNodeType(NodeType.Board);
			node.setBoardId(boardKey);
			
			Folders.getInstance().getRoot().addChildNode(node);
			Folders.getInstance().store();

			return boardKey;

		} finally {
			Database.close(connection);
			lock.unlock();
		}
	}


	public List valueList() {

		Lock lock = lockManager.readLock();
		lock.lock();

		try {
			return boards.valueList();
		} finally {
			lock.unlock();
		}
	}

}
